Repository navigation
docs(skills): a hook that enforces a business rule declares refusals and refuses by code - #33488
objectstack-fleet[bot] wants to merge 4 commits into
Conversation
…red code Co-Authored-By: Claude <noreply@anthropic.com>
…ap the validation pointer Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Contract reviewServed-tier: The diff holds no code, gate or schema file, so no implementing file is in scope. The changeset is the lane seat's ACCEPT at Inputs: card #33257 body and all 3 comments (triage, claim, os-dev-report); PR #33488 body, file list (3 files, +47 / -29) and net diff against Check-runs on the head at render time: 46 runs. 24 completed success, 18 completed skipped, 4 in progress ( ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings:
Implemented-by: VERDICT: PASS Adopted by the seat 2026-11-14T22:36Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z |
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Contract reviewServed-tier: This is a delta review. The previous record, comment 6885289276, was a PASS on head Code read on Inputs: comment 6885289276; card #33257 body and all 4 comments (triage 6880063659, claim 6884900591, os-dev-report 6885205230, seat surface-widening record 6885297883); PR #33488 body and file list (3 files, +54 / -37); Check-runs on the head at render time: 42 runs. 22 completed success, 16 completed skipped, 4 in progress ( ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
New findings:
Delta
Implemented-by: VERDICT: PASS Adopted by the seat 2026-11-14T23:05Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z |
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb 维护者速读
席位核对Verdict: ACCEPT, held for the maintainer · head
|
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Correction: contract-review tierThe contract-review record(s) 6885289276, 6885627321 on this PR say The seat re-runs the review at |
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Contract reviewServed-tier: This is the at-tier review of the whole net diff against The diff holds no code, gate or schema file, so no implementing file is in scope. The changeset is the lane seat's ACCEPT at Inputs: card #33257 body and all 5 comments (triage 6880063659, claim 6884900591, os-dev-report 6885205230, surface-widening record 6885297883, ACCEPT note 6885644053); PR #33488 body, file list (3 files, +54 / -37) and the net diff against Check-runs on the head: 53 runs, all completed, 31 success and 22 skipped, none failed and none in progress. Concluded success includes Code read on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings. No must-fix and no should-fix.
Nits alone end the round. A fix to any of them belongs on the prose faces or in a follow-up card; none is required for this head. Implemented-by: VERDICT: PASS Adopted by the seat 2026-11-15T00:41Z · isolated sub-agent at CONTRACT_REVIEW_TIER (served model verified by the seat) · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z |
Requested by Leehom · project thread
Fixes #33257
domain:skills#1 · os-project-manager · session_01FEkQ5njvc1ya6PypTHRBe5
Clause-②: no
Metadata-protocol: none
维护者速读
objectstack-data技能的 hook 参考文档「Declared refusals」一节,开头改为一条规则:执行业务规则的 hook 声明refusals目录、每条规则一个代码、body 抛出该代码;没有目录时随手抛的文字错误会被平台答成HOOK_ABORTED,只适合意料之外的失败。示例换成跨对象状态守卫「只有在库资产才能借出」(ASSET_NOT_IN_STOCK,409),并写明今天只有沙箱jsbody 能声明refusals,handler写法(含内联 TS)保存时被拒。rules/validation.md里指向该文档的税号 hook 示例同步改为声明TAX_ID_BLOCKED并抛代码。HOOK_ABORTED,用户与 agent 都拿不到有意义的代码和译文;技能原文的验证规则示例恰好教的就是这种写法。skills/objectstack-data/references/data-hooks.md的「Declared refusals」一节;再看skills/objectstack-data/rules/validation.md的税号 hook 示例。git diff origin/main -- skills/,预期新示例声明refusals: { ASSET_NOT_IN_STOCK: … status: 409 }并throw Object.assign(new Error(), { code: 'ASSET_NOT_IN_STOCK' }),税号示例不再有throw new Error('Tax ID is blocked')。skills/**):请批准或合并。What changes
skills/objectstack-data/references/data-hooks.md§ Declared refusals opens with the rule the card asks for: a hook that enforces a business rule declares arefusalscatalogue, one SCREAMING_SNAKE code per rule, and its body throws the code. Without a catalogue, a throw is answeredHOOK_ABORTED, the platform's code, which suits a failure nobody planned and not a rule the user can act on.CREDIT_LIMIT/ON_HOLD, as an unchecked fragment) is replaced by the card's case as a fullos:checkhook:checkout_requires_stock, abeforeInsertonasset_checkoutthat reads the asset throughctx.api(api.read) and throwsASSET_NOT_IN_STOCK(declared at 409) unless the asset's status isin_stock. It fails closed: a checkout with noasset_idor an unknown id is refused too (probed below); an asset the saver cannot read finds nothing, so it is refused the same way (read, not probed).refusalsonmaintoday: only a sandboxedjsbody; ahandlerhook, an inline TS function included, is refused (bullet replaces "Refused at save without ajsbody (e.g. a barehandler)"). Nothing hooks: the refusal catalogue (refusals) cannot translate a handler-authored hook's refusals, and holds one sentence per code — a TS hook has no way to refuse in the user's language #33214 would add is taught.code(default 400), with the hook's code asdeclaredCode(bullet replaces "the envelope carries one asdeclaredCode").onErrorgives no code; Best Practices' "give a thrown error a message the caller can act on" becomes "refuse a business rule by a declared code", linking the section.skills/objectstack-data/rules/validation.md§ External / Custom Validation → Lifecycle Hooks, the hook rule that points todata-hooks.md: the pointer sentence says the body "throws a declared code" (linked to the section) instead of "throws on failure", and itsos:checktax-ID hook declaresrefusals: { TAX_ID_BLOCKED: { message: 'This tax ID is blocked.' } }and throws that code instead ofthrow new Error('Tax ID is blocked'), which is exactly theHOOK_ABORTEDshape the card measured. This file was outside the order's expected surface; the seat approved it in its thread before the edit (pointer sentence plus the example's two lines)..changeset/33257-skills-hook-refuses-by-declared-code.md:'@objectstack/skills': patch, carrying both declaration lines.Why
The card's measured build (gallery debut refresh 2026-11-14, card
asset_lifecycle, recorded build2026-11-14-scenario-asset_lifecycle.json, cited and not re-run: a live run needs the maintainer's word, per the order) enforced "only an in-stock asset can be checked out" with a sandboxed hook that declared norefusals; the probe write was refusedHOOK_ABORTED(400), a platform code. The skill taught the catalogue only as a mechanism, not as the rule for business refusals, andrules/validation.mdtaught the free-text throw outright.Mechanism, read on
origin/mainb5f2f516:packages/spec/src/data/hook.zod.ts:HookSchema.refusalsis a record keyed by a SCREAMING_SNAKE code ofHookRefusalSchema(message: plain text, a{{ }}hole refused;status: 400 to 499, absent means 400).refuseUndeclaredLiteralRefusalCoderefusesrefusalson a hook whosebody.languageis notjs(message fromhookRefusalsWithoutSandboxedBody,packages/spec/src/data/hook-refusal-codes.ts:201), and refuses a literally thrown code the catalogue does not declare.packages/runtime/src/sandbox/hook-refusal-catalogue.tsresolveHookRefusal: a body throw carrying a declared code is answered with the entry's message (translated throughhooks.HOOK.refusals.CODE.messagein the caller's locale) asuserMessage, the entry's status (defaultDEFAULT_HOOK_REFUSAL_STATUS400) and the code; an undeclared code, or none, becomes a sandbox fault (500); host-relayed errors and crashes pass through.packages/types/src/thrown-http-error.tsresolveThrownHttpError: a code outside the ADR-0112 ledger resolves the wirecodefrom the status (standardErrorCodeForHttpStatus: 400VALIDATION_ERROR, 409RESOURCE_CONFLICT) and keeps the thrown code asdeclaredCode. A sandbox business throw with no code at a 4xx is answeredUNDECLARED_HOOK_REFUSAL_CODE=HOOK_ABORTED(ledger row inpackages/spec/src/api/error-code-ledger.zod.ts). SoHOOK_ABORTEDdoes not come fromonError: 'abort'as such:onError: 'abort'(the default) only makes the throw abort the write; the code comes from the throw carrying none.Acceptance notes
refusals) is not built here; the order rules it a proposal in the report. Proposed there with the four-axis reading.data-hooks.mdis left at 10011 with 2 tokens of headroom andvalidation.mdat 2761 with 1; no ceiling moved in either direction (scripts/is outside this card's surface).skills/was not swept beyond the two files the card names; the order's serial constraints keepobjectstack-data/SKILL.md(PR docs(skills): the object enable table names aiReadable #33458), the data evals file (PR docs(skills): parent-path record-currency amounts admit no grouping; data eval for the parent-path currencyField #33394) andrules/security.md(card skills: the security rules do not teach a position'srenamedFrom(a renamed position carries its holders) — PR #33008's Skills-docs follow-up #33044) untouched.Verification
Head
47af59e9, baseorigin/mainb5f2f516.Both new examples run in the real runtime (scratch probe, not committed)
A throwaway vitest file in
packages/runtime/src/sandbox/, on the harness ofoverlap-hook-template.integration.test.ts(realObjectQL, SQLite driver,QuickJSScriptRunner,bindHooksToEngine, answers read offmapDataError), each hook passed throughHookSchema.parsefirst. Run after building runtime's dependency closure under the verify lock (VERDICT command-exit 0); then deleted (git statusclean). Result: Test Files 1 passed, Tests 3 passed, exit 0.checkout_requires_stock: an in-stock asset checks out; anin_repairasset, a checkout with noasset_id, and an unknown id each answer409 {"error":"Only an in-stock asset can be checked out.","code":"RESOURCE_CONFLICT","declaredCode":"ASSET_NOT_IN_STOCK","userMessage":"Only an in-stock asset can be checked out."}; one checkout row stored.crm_tax_id_blocklist_check(new shape):400 {"error":"This tax ID is blocked.","code":"VALIDATION_ERROR","declaredCode":"TAX_ID_BLOCKED","userMessage":"This tax ID is blocked."}.400 {"error":"Tax ID is blocked","code":"HOOK_ABORTED"}, the card's symptom.HookSchema.safeParseof a hook withrefusalsand a stringhandler, and with an inline functionhandler: bothsuccess: false, one issue atrefusals("refusalsis answered only for a sandboxedbody(language: 'js') ...").Line and token budget (skills/**)
skills/objectstack-data/references/data-hooks.md: 884 to 893 lines; tokens 9962 to 10009 (ceiling 10011, headroom 2). Paid in the same file: the protocol-17 history sentence under "An unevaluable condition ABORTS" (its point is restated by the paragraph after it), that paragraph shortened, and the old fragment example.skills/objectstack-data/rules/validation.md: 331 to 330 lines; tokens 2754 to 2760 (ceiling 2761, headroom 1). Paid by deleting the "Date range validation" line from Common Script Patterns, which restates thedate_rangecross_fieldexample in the same file.skills/*/SKILL.md: 4603 to 4603 lines (no SKILL.md touched). Bundle total (whole shipped tree): 244659 to 244712 tokens.node scripts/check-skills-token-ratchet.mjs: exit 0.node scripts/pm/check-skill-line-ratchet.mjs: exit 0 (neither file is on its register). No ceiling raised.Derived gates: 45 derived, 44 run green, 1 NOT MEASURED
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 45 families for the three paths. All run on head47af59e9, exit captured before any pipe, after building@objectstack/lint...,@objectstack/client-react...and@objectstack/runtime^...under the verify lock (VERDICT command-exit 0, 320s):check-skills-token-ratchet(scan and--self-test),check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check:doc-authoring,check:skill-links,check:skill-identifier-liveness,check:skill-frame-sync,check:corpus-claim-drift,check:nul-bytes,check:pm-skill-id-lint,@objectstack/lint check:doc-formula-expressions,@objectstack/spec check:skill-refs,check:dts-closure,check:lean-entry-closure,check:sourcemap-no-sources-content.check:dual-build-cjs-loads(reads every package's dist; no package source is touched, so no full build was paid locally; CI builds and runs it).--ranreconciliation: "45 derived famil(ies) accounted for — 44 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".pnpm --filter @objectstack/spec check:skill-examplesexit 0, "346 prose examples type-check across 3 surface(s)", which covers the new and the editedos:checkblocks.Tier and governed readings
dispatch-gates --tier: MANDATORY contract-review tier, bothskills/**paths under clause ① (the published catalog); no one-line exit for this surface.node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-g605co: exit 3, GOVERNED, landing tier H (人合). 2 of 3 paths hit the register (skills/**x2); the changeset is not on it. Size +47 / -29, under the 3000 threshold.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5njvc1ya6PypTHRBe5
Round 1 (head
652a4a09, after contract review 6885289276)data-hooks.mdCommon Patterns "1. Preventing Deletion (sandboxedbody)" now declaresrefusalsSTRATEGIC_ACCOUNT(400) andOPEN_OPPORTUNITIES(409) and throws each by code. The second message drops the count, because a catalogue message takes no parameters.body's throw is answeredHOOK_ABORTED";onError"gives no code of the hook's own".ctx.sessionrow.652a4a09: 45 derived gates, 44 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads). Scratch runtime probe, 4 passed: the guard answers 400VALIDATION_ERRORwithdeclaredCode: STRATEGIC_ACCOUNT, then 409RESOURCE_CONFLICTwithdeclaredCode: OPEN_OPPORTUNITIES, and deletes an account whose only opportunity is closed.